﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="FakeRepository`2.cs" company="http://mohamedradwan.wordpress.com">
//   © 2011 M.Radwan. All rights reserved
// </copyright>
// <summary>
//   The fake repository class will has all the feature of other fake repositories classes but it's a default for complex type that has only one nested type, remember it
//   inherit from BaseFakeRepostiroy which give it all features for CRUD and fake any simple or complex type but it's default is complex type that has only one nested type
// </summary>
// --------------------------------------------------------------------------------------------------------------------

#region

using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;

using M.Radwan.DevMagicFake.Abstract;
using M.Radwan.DevMagicFake.DataGeneration;

#endregion

namespace M.Radwan.DevMagicFake.FakeRepositories
{
    /// <summary>
    /// The fake repository class will has all the feature of other fake repositories classes but it's a default for complex type that has only one nested type, remember it 
    ///   inherit from BaseFakeRepostiroy which give it all features for CRUD and fake any simple or complex type but it's default is complex type that has only one nested type
    /// </summary>
    /// <typeparam name="T">
    /// The Main object that we will need to save or link any nested type to it
    /// </typeparam>
    /// <typeparam name="TX">
    /// The nested type that we will need to link it to the Main type
    /// </typeparam>
    public class FakeRepository<T, TX> : BaseFakeRepository, IRepository<T> where T : class
    {
        #region Public Methods

        /// <summary>
        /// The save method, this method will save list of the Main object and retrieve the nested object by the Id and link it to the main object, for example if we have 
        ///   scenario like we have Vendor that provide Products, we entered all Vendors, then we enter product and choose the appropriate vendor from dropdown list that has 
        ///   Id and name of the Vendor, We don't need to Add the Vendor because it's already saved, we just want to save the product and link it to this vendor, 
        ///   so DevMagicFake will retrieve the Vendor by Id and save it in the property Vendor in the product and then save the product to the MemroyDb
        /// </summary>
        /// <param name="list">
        /// The list of the Main object we want to save.
        /// </param>
        /// <returns>
        /// return the list of the Main object and each object in the list included it's new Id generated by DevMagicFake in cases of Add and link nested object
        /// </returns>
        public List<T> Add(List<T> list)
        {
            foreach (T obj in list)
            {
                this.Add(obj);
            }

            return list;
        }

        /// <summary>
        /// The save all, this method will Add the main object the it's nested so it will not retrieve the nested object by it's Id instead it will save the nested object
        ///   with new Id if the Id is 0, or update the current object in the MemoryDb in case the Id already exists
        /// </summary>
        /// <param name="obj">
        /// The Main object that has the nested object that we want to save with nested.
        /// </param>
        /// <returns>
        /// return Main object that include new Id generated by DevMagicFake in case of new object and include the nested object that include new Id also that generated 
        ///   by DevMagicFake
        /// </returns>
        public T AddAll(T obj)
        {
            string typeFullName = typeof(T).FullName;
            string nestedTypeFullName = typeof(TX).FullName;
            string nestedTypeName = typeof(TX).Name;

            if (nestedTypeName == "List`1" || nestedTypeName == "IEnumerable`1" || nestedTypeName == "IList`1" || nestedTypeName == "IQueryable`1")
            {
                return RepositoryUtilities.SaveCollectionAndSaveNested<T, TX>(nestedTypeFullName, obj, this.MemoryDb, typeFullName);
            }

            TX nestedObject = RepositoryUtilities.ExtractNestedObjectFromParentObject<T, TX>(nestedTypeFullName, obj);
            RepositoryUtilities.SaveObject(this.MemoryDb, obj, typeFullName);
            RepositoryUtilities.SaveObject(this.MemoryDb, nestedObject, nestedTypeFullName);
            return obj;
        }

        /// <summary>
        /// The save all, this method will Add a list of the main objects and the their nested objects, so it will not retrieve the nested object by it's Id, instead it will 
        ///   save the nested objects with new Id if the Id is 0, or update the current object in the MemoryDb in case the Id already exists
        /// </summary>
        /// <param name="list">
        /// The list of main object that has the nested object that we want to save with nested.
        /// </param>
        /// <returns>
        /// return a list of the main objects that include new Id for each main object generated by DevMagicFake in case of new object and include the nested objects that include new Id also for each nested in each main object
        /// </returns>
        public List<T> AddAll(List<T> list)
        {
            foreach (T obj in list)
            {
                this.AddAll(obj);
            }

            return list;
        }

        /// <summary>
        /// The rule uses class property.
        /// </summary>
        /// <param name="predicate">
        /// The predicate.
        /// </param>
        /// <param name="option">
        /// The option.
        /// </param>
        /// <typeparam name="M">
        /// </typeparam>
        /// <returns>
        /// </returns>
        public FakeRepository<T, TX> RuleUsesClassProperty<M>(Expression<Func<T, M>> predicate, Expression<Func<DataGenerationOption, List<M>>> option)
        {
            return (FakeRepository<T, TX>)base.RuleUsesClassProperty(predicate, option);
        }

        #endregion

        #region Implemented Interfaces

        #region IRepository<T>

        /// <summary>
        /// The Add method one of the main feature of the Dev Magic Fake, it does many of the magic stuff so it's save a new instance if it's Id is 0 or if it's Id is not exist, it update the instance if it's Id existing in the MemoryDb
        /// </summary>
        /// <param name="entity">
        /// The object that we want to save or update.
        /// </param>
        /// <returns>
        /// return the object itself, if this is a new object DevMagicFake will assign a new Id incremental for each type,  if this is an existing object, it will be just updated
        /// </returns>
        public T Add(T entity)
        {
            string typeFullName = typeof(T).FullName;
            string nestedTypeFullName = typeof(TX).FullName;
            string nestedTypeName = typeof(TX).Name;

            if (nestedTypeName == "List`1" || nestedTypeName == "IEnumerable`1" || nestedTypeName == "IList`1" || nestedTypeName == "IQueryable`1")
            {
                return RepositoryUtilities.SaveCollection<T, TX>(nestedTypeFullName, entity, this.MemoryDb, typeFullName);
            }
           
            TX tembNestedObject = (TX)Activator.CreateInstance(typeof(TX));
            PropertyInfo nestedTypePropertyInfo = typeof(T).GetProperty(string.Empty); // just initialize 
            var propertyInfos = typeof(T).GetProperties();
            foreach (var propertyInfo in propertyInfos)
            {
                if (propertyInfo.PropertyType.IsInstanceOfType(tembNestedObject))
                {
                    nestedTypePropertyInfo = propertyInfo;
                }
            }

            if (nestedTypePropertyInfo == null)
            {
                throw new ArgumentException(string.Format("Your object didn't has nested Type of {0}, Please check your parent type for nested type", nestedTypeFullName));
            }

            PropertyInfo nestedTypeIdInfo = nestedTypePropertyInfo.PropertyType.GetProperty("Id");
            TX nestedObject = (TX)nestedTypePropertyInfo.GetValue(entity, null);
            long nestedObjectId = (long)nestedTypeIdInfo.GetValue(nestedObject, null);
            nestedObject = RepositoryUtilities.GetObjectById<TX>(this.MemoryDb, nestedObjectId);
            if (nestedObject != null)
            {
                nestedTypePropertyInfo.SetValue(entity, nestedObject, null);
            }

            RepositoryUtilities.SaveObject(this.MemoryDb, entity, typeFullName);
            return entity;
        }

        /// <summary>
        /// The Get method will get an instance based on Expression tree
        /// </summary>
        /// <param name="where">
        /// The where condition that filter the object from the repository as an Expression tree
        /// </param>
        /// <returns>
        /// Instance of type T
        /// </returns>
        public T Get(Expression<Func<T, bool>> where)
        {
            return this.Get<T>(where);
        }

        /// <summary>
        /// Get All object of Type T from MemoryDb, even if we create Fake of T, we don't need this method we can direct query the MemoryStorge <see cref="MemoryStorage"/> using LINQ
        /// </summary>
        /// <returns>
        /// Return a list of all object from Type T in the MemoryDb
        /// </returns>
        public IEnumerable<T> GetAll()
        {
            return this.GetAll<T>();
        }

        /// <summary>
        /// Get an object by code, but remember the object must has a property it's name is code and of type string
        /// </summary>
        /// <param name="code">
        /// The code that we will use to search to get the object that has it
        /// </param>
        /// <returns>
        /// The object that match the code parameter
        /// </returns>
        public T GetByCode(string code)
        {
            return this.GetByCode<T>(code);
        }

        /// <summary>
        /// Get an object by Id, but remember the object must has a property it's name is Id and of type long
        /// </summary>
        /// <param name="id">
        /// The id that we want to search by.
        /// </param>
        /// <returns>
        /// The object that match the Id and from Type T
        /// </returns>
        public T GetById(long id)
        {
            return this.GetById<T>(id);
        }

        /// <summary>
        /// The GetMany method will get many of objects or list of objects based on Expression tree
        /// </summary>
        /// <param name="where">
        /// The where condition that filter the object from the repository as an Expression tree
        /// </param>
        /// <returns>
        /// List of objects from type T
        /// </returns>
        public IEnumerable<T> GetMany(Expression<Func<T, bool>> where)
        {
            return this.GetMany<T>(where);
        }

        /// <summary>
        /// The remove method will delete an object from our repository
        /// </summary>
        /// <param name="entity">
        /// The entity that we want to remove
        /// </param>
        public void Remove(T entity)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// The remove method will delete an object from our repository
        /// </summary>
        /// <param name="where">
        /// The Expression that will be evaluated, the method will delete any object evaluate true for this expression
        /// </param>
        public void Remove(Expression<Func<T, bool>> where)
        {
            throw new NotImplementedException();
        }

        #endregion

        #endregion
    }
}